Skip to content

plugin/scripts: Windows (Git Bash) support#9

Merged
yyiilluu merged 3 commits intoReflexioAI:mainfrom
Nedlitex:windows-support
May 3, 2026
Merged

plugin/scripts: Windows (Git Bash) support#9
yyiilluu merged 3 commits intoReflexioAI:mainfrom
Nedlitex:windows-support

Conversation

@Nedlitex
Copy link
Copy Markdown
Contributor

@Nedlitex Nedlitex commented Apr 27, 2026

Summary

Fixes five Windows-specific bugs in the plugin shell scripts so claude-smart installs and auto-starts cleanly under Git Bash (MINGW64) on Windows 10/11. Every change is gated on claude_smart_is_windows (uname matches MINGW*/MSYS*/CYGWIN*), so Linux and macOS behaviour is byte-for-byte unchanged.

Hit while installing the plugin on Windows 11 with claude-smart 0.1.17. The dashboard's /claude-smart:dashboard slash command failed with backend: not running and dashboard: .next missing, because the install never completed and the service-start scripts couldn't detach.

What was broken

# Where Symptom Cause
1 smart-install.sh uv install uv install reports success then uv sync fails because uv.exe is corrupt astral.sh's bash installer (install.sh) downloads a zip and unzips it; Git Bash's bundled unzip corrupts the Windows uv.exe (bad CRC on inflate).
2 smart-install.sh cs-cite heredoc Settings.json patch silently fails python3 is the Microsoft Store App Execution Alias stub at WindowsApps\python3.exe. command -v python3 returns truthy but invoking it prints "Python was not found" and exits non-zero.
3 _lib.sh claude_smart_spawn_detached Detach falls into broken python3 branch macOS fallback uses python3 -c '...os.setsid()...' — same App Execution stub fires; even with a real Python, os.setsid() is POSIX-only and would AttributeError.
4 backend-service.sh / dashboard-service.sh kill_group stop doesn't kill the service kill -TERM -- "-pgid" (negative pid = process group) is POSIX-only. Windows has no process groups, so the signal goes nowhere.
5 Service scripts inline detach blocks Same as #3, plus PID-file recording assumes pid==pgid which doesn't hold on Windows Inline copies of the setsid/python3/nohup chain in both service scripts.

What changes

_lib.sh — three new helpers, one rewritten:

  • claude_smart_is_windowsuname -s test for MINGW*|MSYS*|CYGWIN*.
  • claude_smart_resolve_python — probes interpreters with -V to filter the App Execution Alias stub. Prefers python over python3 on Windows; preserves python3-first ordering on POSIX.
  • claude_smart_kill_tree — POSIX: signal the process group (TERM, grace, KILL). Windows: taskkill //T //F //PID. Walks Git Bash's MSYS-pid to Windows-pid translation via ps (column 4 in default output is WINPID); $! for a backgrounded job returns the MSYS pid, but taskkill needs the native Windows pid.
  • claude_smart_spawn_detached — Windows path: plain nohup (no setsid, no process groups; nohup ignores SIGHUP, which is sufficient for the child to survive the parent console closing). POSIX path: setsid → resolved-python os.setsid() → nohup, with the python branch now gated on -V so the App Execution stub never fires.

smart-install.sh — Windows branch in the uv-install block uses the official PowerShell installer (irm https://astral.sh/uv/install.ps1 | iex); same destination (~/.local/bin/uv.exe), so the post-install PATH prepend works uniformly. The cs-cite settings.json patcher uses claude_smart_resolve_python instead of bare python3. Also adds ~/.local/bin/uv.exe to the post-install fallback search.

backend-service.sh / dashboard-service.sh — drop the inline setsid/python3/nohup chains; call claude_smart_spawn_detached and record $!. PID-file logic split per-OS:

  • Windows: record the spawned MSYS pid; claude_smart_kill_tree translates to WINPID at signal time.
  • POSIX with setsid: record the pid (== pgid by setsid contract).
  • POSIX fallback: derive pgid via ps -o pgid as before.

kill_group becomes a one-line wrapper around claude_smart_kill_tree so the cross-OS logic lives in one place.

What's intentionally out of scope

  • reap_port_listeners (backend-service.sh) and the dashboard stop fallback both use lsof, which isn't on Git Bash by default. Existing command -v lsof guards already make these no-ops on Windows. Replacing them with netstat -ano | findstr LISTENING parsing would work but expands surface area. The recorded-pid path with taskkill /T /F already covers the normal stop case (kills npm plus its node children).
  • Pinning uv sync --python <ver>: while building this PR I hit a separate failure where uv defaulted to Python 3.14 and hdbscan (transitive via reflexio-ai) has no 3.14 wheel for Windows, so the source build fired and failed on a 3.14 incompatibility. That's a transitive-dep issue, not a Windows-bash issue, and would also bite Linux/macOS users who install Python 3.14. Worth fixing upstream (probably by pinning in pyproject.toml or via a --python flag), but I left it out so this PR stays focused on the bash-vs-Windows angle.
  • .env file encoding: reflexio's .env scaffolding wrote a CP1252-encoded em-dash (byte 0x97) into a comment, which python-dotenv then rejected on the backend at startup. Repository for that fix is ReflexioAI/reflexio, not this one.

Test plan

  • bash -n clean on all four edited scripts.
  • claude_smart_is_windows returns 0 on Git Bash MINGW64 (Windows 11), 1 on Linux/macOS shells.
  • claude_smart_resolve_python returns the real python interpreter on Windows when both real-Python and the WindowsApps stub are on PATH; returns python3 on POSIX.
  • claude_smart_spawn_detached + claude_smart_kill_tree round-trip: spawn ping -n 100 127.0.0.1, verify kill -0 reports alive, verify tasklist shows PING.EXE with the matching WINPID, kill via helper, confirm process gone from tasklist and kill -0 reports dead.
  • smart-install.sh end-to-end on Windows 11 / Git Bash: uv installs via PowerShell (no unzip corruption), uv sync completes (with Python 3.12 — see the "out of scope" note above), dashboard npm install + npm run build produce a working .next/.
  • Backend (reflexio services start --only backend --no-reload) and dashboard (npm run start) reach /health after manual launch via the helpers; /claude-smart:dashboard opens http://localhost:3001 and renders.
  • Validation on Linux + macOS — code paths are gated on claude_smart_is_windows so behaviour should be unchanged, but a maintainer with those environments should re-run the existing service start/stop tests to confirm.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Cross-platform helpers to detect Windows and pick a working Python runtime
    • Installer supports a Windows install flow and detects Windows executables
  • Bug Fixes

    • Consolidated background service start/record/teardown for consistent cross-platform behavior
    • Safer, OS-aware process-tree termination
    • Allowlist update now skips with a warning if no usable Python runtime is found

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Adds Windows-aware process helpers and Python resolver, and refactors service/start/stop and installer scripts to use them for cross-platform detached spawning, PID recording, and process-tree termination.

Changes

Cross-platform process lifecycle

Layer / File(s) Summary
Data Shape / Helpers
plugin/scripts/_lib.sh
Adds claude_smart_is_windows() and claude_smart_resolve_python(); extends claude_smart_spawn_detached() with Windows vs non-Windows branches and stdin redirection; adds claude_smart_kill_tree(pid) implementing OS-aware termination with escalation.
Spawn wiring / Start
plugin/scripts/backend-service.sh, plugin/scripts/dashboard-service.sh
Replace ad-hoc setsid/Python-setsid/nohup logic with claude_smart_spawn_detached ... for detached starts; preserve log redirection.
PID recording
plugin/scripts/backend-service.sh, plugin/scripts/dashboard-service.sh
Record the spawned PID returned by the detached spawn directly into backend.pid/dashboard.pid (no ps-derived PGID fallback).
Teardown / Kill wiring
plugin/scripts/backend-service.sh, plugin/scripts/dashboard-service.sh
Replace bespoke SIGTERM→wait→SIGKILL PGID loops with calls to claude_smart_kill_tree to terminate process trees across platforms.
Installer adjustments
plugin/scripts/smart-install.sh
Add Windows installer path via PowerShell (requires powershell), include ~/.local/bin/uv.exe in uv detection, and replace direct python3 usage with claude_smart_resolve_python for cs-cite allowlist (skip with warning if no interpreter found).

Sequence Diagram(s)

sequenceDiagram
  participant ServiceScript as Service script
  participant Helper as claude_smart_* helpers
  participant OS as Operating System / Process table
  ServiceScript->>Helper: claude_smart_spawn_detached(cmd...)
  Helper->>OS: spawn detached process (setsid/nohup/Win)
  OS-->>Helper: child PID
  Helper-->>ServiceScript: returns svc_pid
  ServiceScript->>OS: write svc_pid to PID file
  ServiceScript->>OS: probe /health

  ServiceScript->>Helper: claude_smart_kill_tree(svc_pid)
  Helper->>OS: taskkill or kill (TERM -> wait -> KILL)
  OS-->>Helper: termination result
  Helper-->>ServiceScript: exit status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through scripts at dawn's soft light,

Found PIDs to summon, and ghosts to right,
Windows or Unix, I learned each way to send,
Spawned with care, then bade the tree to end,
A small rabbit's fix — cross-platform delight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding Windows (Git Bash) support to plugin/scripts, which is the primary focus of all changes across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugin/scripts/_lib.sh`:
- Around line 42-59: The POSIX branch of claude_smart_resolve_python currently
only checks discoverability with command -v but must also verify the interpreter
actually runs like the Windows branch; update the for-loop for cand in python3
python to, after command -v "$cand", attempt a lightweight runtime check (e.g.,
"$cand" -V or --version redirected to /dev/null) and only return that candidate
if the check succeeds so that claude_smart_spawn_detached() will receive a
working interpreter; keep the same candidate order and error handling (return 1
if none pass).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 18d623bb-c99a-403a-a30b-f6680f6fd22a

📥 Commits

Reviewing files that changed from the base of the PR and between e412c32 and 0bfaebb.

📒 Files selected for processing (4)
  • plugin/scripts/_lib.sh
  • plugin/scripts/backend-service.sh
  • plugin/scripts/dashboard-service.sh
  • plugin/scripts/smart-install.sh

Comment thread plugin/scripts/_lib.sh
Nedlitex and others added 2 commits May 3, 2026 09:39
Five Git-Bash-on-Windows bugs prevented the plugin install + service
auto-start from working. None of them affect Linux/macOS — every change
is gated on `claude_smart_is_windows` (uname matches MINGW*/MSYS*/CYGWIN*).

1. uv install: the astral.sh bash installer downloads a zip and unzips
   it, but Git Bash's bundled unzip corrupts the Windows uv.exe (bad CRC
   on the inflated binary). On Windows, run the official PowerShell
   installer (install.ps1) instead — same destination
   (~/.local/bin/uv.exe), so the post-install PATH prepend works
   uniformly.

2. python3 App Execution Alias stub: on Windows,
   %LocalAppData%\Microsoft\WindowsApps\python3.exe is a Microsoft Store
   launcher stub. `command -v python3` returns truthy but invoking it
   prints "Python was not found" and exits non-zero. Add
   claude_smart_resolve_python which probes interpreters with `-V` to
   filter the stub out, preferring `python` (the real installed one)
   over `python3` on Windows. Used by smart-install.sh's settings.json
   patcher and by the spawn-detached fallback.

3. spawn_detached on Windows: Git Bash has no setsid, no process groups,
   and os.setsid() is POSIX-only — the existing python3-based fallback
   would never work even if a real Python were on PATH. Use plain nohup
   on Windows: ignoring SIGHUP is sufficient for the child to survive
   the parent console closing, and Windows doesn't have the
   zombie-on-parent-exit problem POSIX has.

4. kill_tree on Windows: kill -TERM -- "-pgid" (negative pid = process
   group) is a POSIX construct with no Windows equivalent. Use taskkill
   /T /F /PID, which walks the child-process tree via the
   parent-pid/job-object relationships. Subtlety: in Git Bash, $! for a
   backgrounded job returns the MSYS pid (an internal counter), not the
   native Windows pid taskkill needs. ps's default output exposes the
   WINPID column; awk extracts it. Service scripts now delegate
   kill_group to claude_smart_kill_tree so the logic stays in one place.

5. Service start scripts (backend-service.sh, dashboard-service.sh):
   the inline setsid/python3/nohup detach blocks would land in the
   broken python3 branch on Windows. Replace with the unified
   claude_smart_spawn_detached helper. PID-file recording is split:
   Windows records the spawned MSYS pid (translated to WINPID by
   kill_tree at signal time); POSIX still records the pgid where setsid
   guarantees pid==pgid, and falls back to ps -o pgid otherwise.

Tested on Windows 11 / Git Bash (MINGW64): smart-install.sh now
completes (uv install + uv sync + dashboard build), and
claude_smart_spawn_detached + kill_tree round-trip a `ping -n 100`
through start-alive-kill-gone end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yyiilluu yyiilluu force-pushed the windows-support branch from 0bfaebb to 5f6c258 Compare May 3, 2026 16:39
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin/scripts/dashboard-service.sh (1)

129-139: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid the PGID race on the Python-detach path.

When setsid is unavailable but claude_smart_spawn_detached uses the Python os.setsid() branch, the ps -o pgid= lookup here can run before the child has entered its new session. That can persist the parent PGID, so stop may miss the dashboard or signal the wrong process group on macOS/fallback POSIX.

💡 Suggested fix
     claude_smart_spawn_detached npm run start >>"$LOG_FILE" 2>&1
     dash_pid=$!
     if claude_smart_is_windows; then
       echo "$dash_pid" > "$PID_FILE"
     elif command -v setsid >/dev/null 2>&1; then
       echo "$dash_pid" > "$PID_FILE"
+    elif _CS_PY=$(claude_smart_resolve_python) && [ -n "$_CS_PY" ]; then
+      # claude_smart_spawn_detached used python os.setsid(), so pid==pgid.
+      echo "$dash_pid" > "$PID_FILE"
     else
       actual_pgid=""
       if command -v ps >/dev/null 2>&1; then
         actual_pgid=$(ps -o pgid= -p "$dash_pid" 2>/dev/null | tr -d ' ')
       fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/scripts/dashboard-service.sh` around lines 129 - 139, The current PGID
lookup can race when the Python detach path (claude_smart_spawn_detached using
os.setsid()) is used: after starting the dashboard (dash_pid) the ps -o pgid=
query may run before the child joins its new session and return the parent's
PGID. Change the fallback branch (when setsid is unavailable and
claude_smart_is_windows is false) to poll for the child's PGID: repeatedly run
ps -o pgid= -p "$dash_pid" with a short sleep until the returned PGID differs
from the original parent PGID (or until a short timeout), then write that PGID
(or dash_pid if timeout/no ps) to PID_FILE; this ensures the stop logic targets
the correct process group when claude_smart_spawn_detached used Python
os.setsid().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@plugin/scripts/dashboard-service.sh`:
- Around line 129-139: The current PGID lookup can race when the Python detach
path (claude_smart_spawn_detached using os.setsid()) is used: after starting the
dashboard (dash_pid) the ps -o pgid= query may run before the child joins its
new session and return the parent's PGID. Change the fallback branch (when
setsid is unavailable and claude_smart_is_windows is false) to poll for the
child's PGID: repeatedly run ps -o pgid= -p "$dash_pid" with a short sleep until
the returned PGID differs from the original parent PGID (or until a short
timeout), then write that PGID (or dash_pid if timeout/no ps) to PID_FILE; this
ensures the stop logic targets the correct process group when
claude_smart_spawn_detached used Python os.setsid().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b54716f-2c52-4e69-8b8e-6f0ed5f2e8fa

📥 Commits

Reviewing files that changed from the base of the PR and between 0bfaebb and 5f6c258.

📒 Files selected for processing (4)
  • plugin/scripts/_lib.sh
  • plugin/scripts/backend-service.sh
  • plugin/scripts/dashboard-service.sh
  • plugin/scripts/smart-install.sh
🚧 Files skipped from review as they are similar to previous changes (3)
  • plugin/scripts/_lib.sh
  • plugin/scripts/backend-service.sh
  • plugin/scripts/smart-install.sh

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugin/scripts/_lib.sh`:
- Around line 24-33: The claude_smart_is_windows() helper incorrectly treats
CYGWIN as Windows; narrow its case pattern to only MINGW* and MSYS* so CYGWIN
falls through to the POSIX path—update the case in claude_smart_is_windows() to
match only MINGW*|MSYS* and leave CYGWIN* out so callers like
claude_smart_spawn_detached() and claude_smart_kill_tree() will use the POSIX
behavior (setsid/process groups and TERM→KILL semantics) instead of taskkill.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5989a3e1-8971-450e-af46-4629d40643f7

📥 Commits

Reviewing files that changed from the base of the PR and between 5f6c258 and f0f05f6.

📒 Files selected for processing (3)
  • plugin/scripts/_lib.sh
  • plugin/scripts/backend-service.sh
  • plugin/scripts/dashboard-service.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugin/scripts/dashboard-service.sh

Comment thread plugin/scripts/_lib.sh
Comment on lines +24 to +33
# Return 0 (true) if running under a Windows-flavoured bash (Git Bash,
# MSYS, Cygwin). Used to gate POSIX-only primitives (setsid, process
# groups) and route around Windows-specific potholes (the python3 App
# Execution Alias stub at WindowsApps\python3.exe).
claude_smart_is_windows() {
case "$(uname -s 2>/dev/null)" in
MINGW*|MSYS*|CYGWIN*) return 0 ;;
*) return 1 ;;
esac
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Route Cygwin through the POSIX code path, not Windows.

CYGWIN* fully supports POSIX setsid, process groups, and signal-based termination. Routing it through MINGW*|MSYS* branches in claude_smart_spawn_detached() and claude_smart_kill_tree() breaks this: the Windows branch uses taskkill, a Windows-native tool that cannot reliably terminate POSIX Cygwin processes, and it loses the graceful TERM→KILL semantics. Narrow claude_smart_is_windows to MINGW*|MSYS* only.

Suggested change
 claude_smart_is_windows() {
   case "$(uname -s 2>/dev/null)" in
-    MINGW*|MSYS*|CYGWIN*) return 0 ;;
+    MINGW*|MSYS*) return 0 ;;
     *) return 1 ;;
   esac
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Return 0 (true) if running under a Windows-flavoured bash (Git Bash,
# MSYS, Cygwin). Used to gate POSIX-only primitives (setsid, process
# groups) and route around Windows-specific potholes (the python3 App
# Execution Alias stub at WindowsApps\python3.exe).
claude_smart_is_windows() {
case "$(uname -s 2>/dev/null)" in
MINGW*|MSYS*|CYGWIN*) return 0 ;;
*) return 1 ;;
esac
}
# Return 0 (true) if running under a Windows-flavoured bash (Git Bash,
# MSYS, Cygwin). Used to gate POSIX-only primitives (setsid, process
# groups) and route around Windows-specific potholes (the python3 App
# Execution Alias stub at WindowsApps\python3.exe).
claude_smart_is_windows() {
case "$(uname -s 2>/dev/null)" in
MINGW*|MSYS*) return 0 ;;
*) return 1 ;;
esac
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/scripts/_lib.sh` around lines 24 - 33, The claude_smart_is_windows()
helper incorrectly treats CYGWIN as Windows; narrow its case pattern to only
MINGW* and MSYS* so CYGWIN falls through to the POSIX path—update the case in
claude_smart_is_windows() to match only MINGW*|MSYS* and leave CYGWIN* out so
callers like claude_smart_spawn_detached() and claude_smart_kill_tree() will use
the POSIX behavior (setsid/process groups and TERM→KILL semantics) instead of
taskkill.

@yyiilluu yyiilluu merged commit cdaf200 into ReflexioAI:main May 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants